//------------------------------------------------------------------------------
// <copyright company="Telligent Systems">
//     Copyright (c) Telligent Systems Corporation.  All rights reserved.
// </copyright> 
//------------------------------------------------------------------------------

using System;
using System.Runtime.InteropServices;
using System.Globalization;

namespace CommunityServer.Components 
{	
	#region TimeZoneArea
	/// <summary>
	/// An enumeration of the available time zones supported
	/// </summary>
	public enum TimeZoneArea 
	{
		AfghanistanStandardTime = 175,
		AlaskanStandardTime = 3,
		ArabStandardTime = 150,
		ArabianStandardTime = 165,
		ArabicStandardTime = 158,
		AtlanticStandardTime = 50,
		AUSCentralStandardTime = 245,
		AUSEasternStandardTime = 255,
		AzoresStandardTime = 80,
		CanadaCentralStandardTime = 25,
		CapeVerdeStandardTime = 83,
		CaucasusStandardTime = 170,
		CentralAustraliaStandardTime = 250,
		CentralAmericaStandardTime = 33,
		CentralAsiaStandardTime = 195,
		CentralEuropeStandardTime = 95,
		CentralEuropeanStandardTime = 100,
		CentralPacificStandardTime = 280,
		CentralStandardTime = 20,
		ChinaStandardTime = 210,
		DatelineStandardTime = 0,
		EastAfricaStandardTime = 155,
		EastAustraliaStandardTime = 260,
		EastEuropeStandardTime = 115,
		EastSouthAmericaStandardTime = 65,
		EasternStandardTime = 35,
		EgyptStandardTime = 120,
		EkaterinburgStandardTime = 180,
		FijiStandardTime = 285,
		FLEStandardTime = 125,
		GMTStandardTime = 85,
		GreenlandStandardTime = 73,
		GreenwichStandardTime = 90,
		GTBStandardTime = 130,
		HawaiianStandardTime = 2,
		IndiaStandardTime = 190,
		IranStandardTime = 160,
		JerusalemStandardTime = 135,
		KoreaStandardTime = 230,
		MexicoStandardTime = 30,
		MexicoStandardTime2 = 13,
		MidAtlanticStandardTime = 75,
		MountainStandardTime = 10,
		MyanmarStandardTime = 203,
		NorthCentralAsiaStandardTime = 201,
		NepalStandardTime = 193,
		NewZealandStandardTime = 290,
		NewfoundlandStandardTime = 60,
		NorthAsiaEastStandardTime = 227,
		NorthAsiaStandardTime = 207,
		PacificSAStandardTime = 56,
		PacificStandardTime = 4,
		RomanceStandardTime = 105,
		RussianStandardTime = 145,
		SAEasternStandardTime = 70,
		SAPacificStandardTime = 45,
		SAWesternStandardTime = 55,
		SamoaStandardTime = 1,
		SEAsiaStandardTime = 205,
		MalayPeninsulaStandardTime = 215,
		SouthAfricaStandardTime = 140,
		SriLankaStandardTime = 200,
		TaipeiStandardTime = 220,
		TasmaniaStandardTime = 265,
		TokyoStandardTime = 235,
		TongaStandardTime = 300,
		USEasternStandardTime = 40,
		USMountainStandardTime = 15,
		VladivostokStandardTime = 270,
		WestAustraliaStandardTime = 225,
		WestCentralAfricaStandardTime = 113,
		WestEuropeStandardTime = 110,
		WestAsiaStandardTime = 185,
		WestPacificStandardTime = 275,
		YakutskStandardTime = 240,
	};
	#endregion

	#region TimeZoneInternational
	/// <summary>
	/// A class supporting an instance of a timezone within the world. This class is a bit different than the TimeZone
	/// class that is part of the CLR. The CLR TimeZone class only represents the current TimeZone of the computer and
	/// has no facility to convert DateTime from one TimeZone in the world, to the corresponding DateTime in another 
	/// TimeZone in the world.
	/// </summary>
	[Serializable]
	public sealed class TimeZoneInternational : System.TimeZone
	{
		#region Fields
		private readonly long TicksPerMinute = 0x23c34600;
		#endregion

		#region Properties
		/// <summary>
		/// The standard name for this TimeZone regardless of whether the it's in Daylight Saving Time or Standard Time
		/// </summary>
		public string DisplayName 
		{
			get{ return _displayName; }
		}
		/// <summary>
		/// The TimeZoneArea enumerated value that represents this timezone.
		/// </summary>
		public TimeZoneArea TimeZoneArea 
		{
			get{ return _timeZoneArea; }
		}
		/// <summary>
		/// The English name of the TimeZone when in Daylight Saving Time
		/// </summary>
		public override string DaylightName 
		{
			get{ return _daylightName;}
		}
		/// <summary>
		/// The English name of the TimeZone when in Standard Time
		/// </summary>
		public override string StandardName 
		{
			get{ return _standardName;}
		}
		/// <summary>
		/// The number of minutes this TimeZone is offset from UTC when in Daylight Savings Time.
		/// </summary>
		public int DaylightBias
		{
			get
			{ 
				return (this._bias + this._daylightBias);
			}
		}
		/// <summary>
		/// This property returns the number of minutes the TimeZoneInformation is offset from UTC. When the number is positive, the TimeZone is
		/// behind UTC and if the number is positive, the TimeZone is ahead of UTC.
		/// </summary>
		public int StandardBias
		{
			get 
			{ 
				return (this._bias + this._standardBias);
			}
		}

		#endregion

		#region Constructors
		/// <summary>
		/// Private constructor to prevent users from instantiating a new instance of the class
		/// </summary>
		private TimeZoneInternational() 
		{
		}
		/// <summary>
		/// Private contructor to prevent this class from being instantiated with a TimeZone instance
		/// </summary>
		/// <param name="timezone"></param>
		private TimeZoneInternational( System.TimeZone timezone ) 
		{
		}

		/// <summary>
		/// Internal constructor used by the managing class to construct a TimeZoneInternational instance
		/// </summary>
		/// <param name="timeZoneArea">The area the timezone information is for. </param>
		/// <param name="bias">The difference in minutes between Coordinated Universal Time (UTC) and local time</param>
		/// <param name="displayName">The string used to display the timezone. Includes multiple localities to help identify the timezone information.</param>
		/// <param name="standardName">The name of the standard time</param>
		/// <param name="standardDate">The date and time when the transition from daylight saving time to standard time occurs.</param>
		/// <param name="standardBias">Bias value used during local time translations that occur during standard time</param>
		/// <param name="daylightName">The name of the daylight saving time</param>
		/// <param name="daylightDate">The date and time when the transition from standard time to daylight saving time occurs.</param>
		/// <param name="daylightBias">Bias value used during local time translations that occur during daylight saving time.</param>
		internal  TimeZoneInternational( TimeZoneArea timeZoneArea, int bias, string displayName, string standardName, SYSTEMTIME standardDate,  int standardBias, string daylightName, SYSTEMTIME daylightDate, int daylightBias ) 
		{

			this._timeZoneArea	= timeZoneArea;
			this._bias			= bias;
			this._displayName	= displayName;
			this._standardName	= standardName;
			this._standardDate	= standardDate;
			this._standardBias	= standardBias;
			this._daylightName	= daylightName;
			this._daylightDate	= daylightDate;
			this._daylightBias	= daylightBias;
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// A method to return a TimeSpan difference between this TimeZone and the GMT/UTC TimeZone
		/// 
		/// Due to daylight savings time, the UTC offset will differ throughout the year. During DST
		/// the time will be "spring forward" ahead, during Standard Time, the offset will "fall back"
		/// to the normal offset.
		/// </summary>
		/// <param name="localTime">The DateTime to lookup the offset.</param>
		/// <returns></returns>
		public override TimeSpan GetUtcOffset(DateTime localTime) 
		{
			if( IsDaylightSavingTime(localTime) )
				return new TimeSpan(0, 0, this.DaylightBias );
			else
				return new TimeSpan(0, 0, this.StandardBias );
		}

		/// <summary>
		/// A method that will return the local time for this TimeZone when passed the UTC 
		/// </summary>
		/// <param name="utcTime">The UTC DateTime value</param>
		/// <returns></returns>
		public override DateTime ToLocalTime(DateTime utcTime) 
		{
			TimeZoneInternational tzGmt = TimeZoneInternationalManager.TimeZones[TimeZoneArea.GMTStandardTime];

			DateTime localTime = tzGmt.ToUniversalTime(utcTime);

			localTime = localTime.AddMinutes( -(this.StandardBias) );
			if( this.IsDaylightSavingTime(localTime))
				localTime = localTime.AddMinutes(this.StandardBias).AddMinutes( -(this.DaylightBias) );
			
			return localTime;
		}

		/// <summary>
		/// A method that will return the local time from the provided TimeZone and the TimeZone localTime
		/// </summary>
		/// <param name="sourceTime">The local time for the referenced TimeZone</param>
		/// <param name="sourceTimeZone">The TimeZone corresponding to the local time provided</param>
		/// <returns></returns>
		public DateTime ToLocalTime( DateTime sourceTime, TimeZoneInternational sourceTimeZone )	
		{
			DateTime utc = sourceTimeZone.ToUniversalTime(sourceTime);

			return this.ToLocalTime(utc);
		}

		/// <summary>
		/// A method to convert the local time into UTC time
		/// </summary>
		/// <param name="localTime">The DateTime value of the current or local time</param>
		/// <returns></returns>
		public override DateTime ToUniversalTime(DateTime localTime) 
		{
			if(!IsValidTime(localTime) )
				throw new ArgumentException( String.Format("The provided DateTime value ({0}) is not a valid DateTime for this timezone. This could be due to the fact that the time given is during the time that the TimeZone leaps ahead to Daylight Saving Time", localTime ));

			DateTime utc = localTime.AddMinutes( this.StandardBias );
			if( this.IsDaylightSavingTime(utc) )
			{
				return utc.AddMinutes( -(this.StandardBias) ).AddMinutes( this.DaylightBias );
			}
			else
				return utc;
		}

		/// <summary>
		/// A method that will detect whether the local time is in Daylight Saving Time for the current TimeZone
		/// </summary>
		/// <param name="localTime">The DateTime value in local time that will be checked if the time is during the Daylight Saving Time period</param>
		/// <returns></returns>
		public override bool IsDaylightSavingTime(DateTime localTime)
		{
			DaylightTime daylightTime = this.GetDaylightChanges( localTime.Year );
			
			if (daylightTime.Start <= localTime && localTime < daylightTime.End)
			{
				return true;                                           
			}
			else
				return false;
		}

		/// <summary>
		/// A method that will return the DaylightTime values for the current timezone and the provided year
		/// </summary>
		/// <param name="year">The year to calculate the DaylightTime for</param>
		/// <returns></returns>
		public override System.Globalization.DaylightTime GetDaylightChanges(int year) 
		{
			if( year < 1 || (year > 0x270f)) 
			{
				throw new ArgumentOutOfRangeException("year", String.Format("The year {0} is out of the range of possible values {1},{2}", year, 1, 0x270f));
			}
			else 
			{

				if( this.DaylightBias != 0 ) 
				{
					DateTime daylightStartLastDayOfMonth	= new DateTime(year, _daylightDate.Month + 1, 1, _daylightDate.Hour, _daylightDate.Minute, _daylightDate.Second, _daylightDate.Milliseconds ).AddDays(-1);
					DateTime daylightEndLastDayOfMonth		= new DateTime(year, _standardDate.Month + 1, 1, _standardDate.Hour, _standardDate.Minute, _standardDate.Second, _standardDate.Milliseconds  ).AddDays(-1);

					System.Globalization.GregorianCalendar cal = new GregorianCalendar();
					DayOfWeek dowStart = cal.GetDayOfWeek(daylightStartLastDayOfMonth);
					DayOfWeek dowEnd = cal.GetDayOfWeek(daylightEndLastDayOfMonth);

					DateTime start	= DateTime.MinValue;
					DateTime end	= DateTime.MinValue;
					if( _daylightDate.Day == 5 ) // last day of month
						start	= daylightStartLastDayOfMonth.AddDays( -((short)dowStart) );	
					else
						start	= daylightStartLastDayOfMonth.AddDays(	-((short)dowStart + (( 4 - _daylightDate.Day ) * 7 )) );

					if( _standardDate.Day == 5 ) // last day of month
						end = daylightEndLastDayOfMonth.AddDays( -((short)dowEnd) );
					else
						end	= daylightEndLastDayOfMonth.AddDays(	-((short)dowEnd	+ (( 4 - _standardDate.Day ) * 7 )) );
					
					TimeSpan delta = new TimeSpan( (_standardBias - _daylightBias) * TicksPerMinute );

					return new System.Globalization.DaylightTime( start, end, delta );            
				}
				else
					return new System.Globalization.DaylightTime( DateTime.MinValue, DateTime.MinValue, TimeSpan.Zero );
			}
			
		}

		#endregion

		#region Protected Properties
		#endregion

		#region Protected Methods
		#endregion

		#region Private Methods
		/// <summary>
		/// A private method to return the current time on the computer
		/// </summary>
		/// <returns></returns>
		private SYSTEMTIME GetSystemTime()
		{
			return GetSystemTime( DateTime.Now );
		}

		private SYSTEMTIME GetSystemTime(DateTime dateTimeToConvert )
		{
			GregorianCalendar cal = new System.Globalization.GregorianCalendar(GregorianCalendarTypes.Localized);

			SYSTEMTIME sysTime = new SYSTEMTIME( (short)dateTimeToConvert.Year, (short)dateTimeToConvert.Month, (short)cal.GetDayOfWeek( dateTimeToConvert), (short)dateTimeToConvert.Day, (short)dateTimeToConvert.Hour, (short)dateTimeToConvert.Minute, (short)dateTimeToConvert.Second, (short)dateTimeToConvert.Millisecond );

			return sysTime;
		}

		private TIME_ZONE_INFORMATION GetNativeTimeZoneInformation()
		{
			return new TIME_ZONE_INFORMATION(this._bias, this._standardName, this._standardDate, this._standardBias, this._daylightName, this._standardDate, this._daylightBias );
		}

		private bool IsValidTime( DateTime localTime )
		{
			DaylightTime dlTime = this.GetDaylightChanges( localTime.Year );

			// check if the given time is right on the cusp of daylight saving / standard time, if so, this is an invalid datetime 
			// so throw an exception
			if(	(dlTime.Start <= localTime && dlTime.Start.AddMinutes( -(this._daylightBias)) > localTime) )
			{
				return false;
			}
			else
				return true;
		}
		#endregion

		#region Private Data
		private TimeZoneArea	_timeZoneArea;	
		private int				_bias;
		private string			_displayName;
		private string			_standardName;
		private SYSTEMTIME		_standardDate;
		private int				_standardBias;
		private string			_daylightName;
		private SYSTEMTIME		_daylightDate;
		private int				_daylightBias;
		#endregion
	}
	#endregion

	#region SYSTEMTIME
	[StructLayout(LayoutKind.Sequential)]
	internal struct SYSTEMTIME 
	{
		[MarshalAs(UnmanagedType.U2)] public short Year;
		[MarshalAs(UnmanagedType.U2)] public short Month;
		[MarshalAs(UnmanagedType.U2)] public short DayOfWeek;
		[MarshalAs(UnmanagedType.U2)] public short Day;
		[MarshalAs(UnmanagedType.U2)] public short Hour;
		[MarshalAs(UnmanagedType.U2)] public short Minute;
		[MarshalAs(UnmanagedType.U2)] public short Second;
		[MarshalAs(UnmanagedType.U2)] public short Milliseconds;

		public SYSTEMTIME(short year, short month, short dayOfWeek, short day, short hour, short minute, short second, short milliseconds) 
		{
			this.Year = year;
			this.Month = month;
			this.DayOfWeek = dayOfWeek;
			this.Day = day;
			this.Hour = hour;
			this.Minute = minute;
			this.Second = second;
			this.Milliseconds = milliseconds;
		}

		override public string ToString()
		{
			return new DateTime( this.Year, this.Month, this.Day, this.Hour, this.Minute, this.Second, this.Milliseconds ).ToString();
		}
	}
	#endregion

	#region TIME_ZONE_INFORMATION
	[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
	internal struct TIME_ZONE_INFORMATION 
	{
		[MarshalAs(UnmanagedType.I4)]            
		public int Bias;
		[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]    
		public string StandardName;
		public SYSTEMTIME StandardDate;
		[MarshalAs(UnmanagedType.I4)]            
		public int StandardBias;  
		[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]    
		public string DaylightName;
		public SYSTEMTIME DaylightDate;  
		[MarshalAs(UnmanagedType.I4)]            
		public int DaylightBias;

		public TIME_ZONE_INFORMATION( int bias, string standardName, SYSTEMTIME standardDate, int standardBias, string daylightName, SYSTEMTIME daylightDate, int daylightBias )
		{
			Bias = bias;
			StandardName = standardName;
			StandardDate = standardDate;
			StandardBias = standardBias;
			DaylightName = daylightName;
			DaylightDate = daylightDate;
			DaylightBias = daylightBias;
		}
	}
	#endregion
}
